2. Klasser och objekt

 

 

·     Objekt.

 

·     Klasser.

 

·     Deklarera klasser.

 

·     Skapa objekt.

 

·     Använda objekt.

 

·     Constructor/destructor.

 

·     Konstanta objekt.

 

·     Peka på objekt.

 

·     New och delete.

·     Kompilerade klasser.

 

·     Klassarv.

 

·     Klasshierarki.

 

·     Utbyte av medlemsfunktion.

 


Objekt.

 

Om man tar diverse variabler och beräkningar, vilka tillsammans kan beskriva någon typ av objekt, t.ex. en geometrisk figur, ett banklån, en matematisk funk­tion, ett matrecept, en teknisk beskrivning av en molekyl etc, och sätter samman dessa enligt en fördefinierad mall, får vi ett objekt i den mening vi avser under begreppet Objekt-Orienterad Programmering, OOP.

 

För att göra ovanstående abstraktion något mer konkret kan vi ta ett exempel på en geometrisk figur: en cirkel.

 

En cirkel har, matematiskt sett, tre variabler: en radie, en omkrets samt en yta. Dessa tre variabler hänger intimt samman med varandra genom enkla samband, enkla matematiska beräkningar. Man kan t.ex. m.h.a. en känd radie beräkna omkretsen genom sambandet:

 

omkrets = 2 * PI * radie

 

Där PI kan finnas med i början av programmet som en konstant (3.14159...). Man kan också konstatera att:

 

yta = PI * radie * radie

 

Därmed har vi receptet på objektet Cirkel:

 

I.                   Ett antal variabler:

A.               radie

B.                omkrets

C.               yta

II.                Ett antal funktioner:

A.               beräkna omkrets från radie

B.                beräkna yta från radie

Naturligtvis kan man tänka sig att ha fler variabler och funktioner i ett objekt som kallas cirkel, t.ex kanske vi vill beräkna radien om ytan är känd, eller så vill vi kunna rita upp cirkeln på bildskärmen, och då kan det vara önskvärt med färg på cirkeln, tjocklek på linjen, fyllnadsfärg etc.

 

Övningsuppgift:

·      Beskriv på samma sätt hur de övriga objekten som nämns i 1:a stycket
kan se ut.

När man skriver i C kan man tänkas vilja representera ett datum med en struktur, sedan skapa en strukturvariabel, och initiera dess element:

 

struct datum

{

    int aar;

    int manad;

    int dag;

};

 

struct datum foedelse_datum;

 

foedelse _datum.aar    = 1951;

foedelse _datum.maanad = 3;

foedelse _datum.dag    = 25;

 

Nu kan man ju inte skriva ut detta datum bara genom att skicka 'foedelse_datum' som argument till printf(). Man måste antingen skicka varje element för sig till printf(), eller skriva en egen utskriftsfunktion (som i sin tur kan använda printf()). Låt oss kalla den visa_datum():

 

void visa_datum(struct datum * d)

{

    static char cMaanad[][10] =

                {"Januari", "Februari", "Mars",

                 "April",   "Maj",      "Juni",

                 "Juli",    "Augusti",  "September",

                 "Oktober", "November", "December"};

 

    printf("%d %s %d\n",

           d->dag, cMaanad[(d->maanad) - 1], d->aar);

}

 

Vill man t.ex. jämföra två datum måste man jämföra ett element i taget, och använda viss logik i jämförelsen. Om man jämför 1995-12-10 med 1996-01-05 är både dag och månad lägre för det andra datumet trots att det första inträffade tidigare. P.s.s. som vid utskrift kan man skapa en egen funktion som tar två pekare till struct datum som argument, och returnerar ett svar. Skulle svaret kanske kunna fungera som returvärdet från strcmp()?

 

Övningsuppgift.

·      Skapa ovanstående datum-struktur.

·      Skriv en jämförelsefunktion. Slå upp strcmp() i hjälpen och använd samma modell för returvärdet.

·      Låt ditt program be användaren ange två datum, mata in dessa i två struct datum-variabler och jämför dem med din funktion.

·      Programmet ska sedan tala om vilket datum som inträffade först. Om datumen är lika ska det rapporteras.

·      Programmet ska fortsätta med att ta emot datumpar tills användaren anger en nolla som svar.

 

 

Denna metod har två uppenbara nackdelar:

 

Man har ingen garanti för att data är korrekt. Strukturen ovan kan t.ex. innehålla ett datum 31 februari 9119. Man kan då inte lätt se vilken del av programmet som skapat detta felaktiga datum. Det kan även vara något som kompilerats i förväg, där vi kanske inte ens har källkoden.

 

När man väl börjat använda 'datum' i sina program har man bundit sig till det format man bestämt. Det går inte lätt att byta format till t.ex. någon form av packat datumformat, som man gjort i time_t.

 

Skulle man dessutom få in ett månadsnummer utanför intervallet 1 - 12, skulle utskriftsrutinen uppföra sig konstigt i och med att vi använder månadsnummer som index i en lista.

 

En skillnad mellan C och C++ är att man designade C++ speciellt på så sätt att det skulle vara så effektivt som möjligt att skapa egendefinierade typer, vilka enkelt kunde handha datakontroll, datasäkerhet, flexibilitet etc.

 

I C++ skapar man både den nya datatypen och de operationer som är avsedda för datat i en och samma modul, en klass, 'class'. En klass består av databeskrivningar och beskrivningar av de operationer som utförs på data.

 


Klasser.

 

En klass är en mall för ett objekt på samma sätt som en strukturmall är en mall för en egendefinierad strukturtyp. Skillnaden ligger i klassens förmåga att ta med funktioner i beskrivningen.

 

En klassdeklaration liknar en strukturdeklaration, åtminstone till sin huvudsakliga utformning. Skillnaden ligger i att man byter ut nyckelordet 'struct' mot 'class', samt att man kan ha många fler finesser i klassen. De mest slående är att man kan ha funktioner, och att man kan gömma data i en klass.

 

Låt oss testa hur långt en strukturmall tar oss när vi försöker beskriva en cirkel:

 

struct Cirkel

{

    float radie;

    float omkrets;

    float yta;

};

 

Man kan inte lägga till en variabel PI som initieras till 3.14159, eftersom vi här bara talar om typer, inte variabler. Variablerna skapas först när man deklarerar en variabel av den nya struct-typen. I van­lig C-programmering brukar man defi­n­iera sådant i början av programmet. Samma sak gäller för klasser. Däremot kan man som vi senare ska se lägga in initieringar vid skapandet av ett objekt.

 

Med hjälp av denna mall kan vi skapa variabler av typen 'struct Cirkel':

 

struct Cirkel Frisbee;

struct Cirkel Ring;

 

Bägge dessa variabler har sin egen radie, sin egen omkrets och sin egen yta. Man kan t.ex. använda elementoperatorn för att lägga in en radie för Frisbee:

 

Frisbee.radie = 24.0F;

 

Vad de däremot inte har är inbakade funktioner för beräkning av omkrets och yta, vilket skulle kunna göras när radien är känd.

 

I vanlig C-programmering lägger man nu till fristående funktioner, vilka kan anropas med anropsparametrar och svara med returvärde. Då skulle man kunna använda elementoperatorn:

 

float radie_till_yta(float radie)

{

    return PI * radie * radie;

}

 

Ovanstående funktion skulle kunna anropas i huvudprogrammet på t.ex. detta sätt (om vi redan deklarerat strukturen Cirkel någonstans):

 

void main(void)

{

    struct Cirkel Ring;

 

    Ring.radie = 12.0F;

    Ring.yta = radie_till_yta(Ring.radie);

}

 

Men vi skulle faktiskt kunna göra på ett annat sätt, vilket verkar bäddat för strul:

 

void main(void)

{

    struct Cirkel Ring;

    float r;

 

    ...

    r = 12.0F;

    ...

 

    Ring.radie = r;

    Ring.yta = radie_till_yta(r);

}

 

Vad skulle hända om vi råkade ändra variabeln r mellan de två sista raderna. Vi kan ju t.ex. göra ändringar i programmet där vi flyttar lite på koden, eller gör ett funktionsanrop mellan dessa två rader, och får variabeln ändrad. Då har vi en struktur med inkoncistent data!

 


Deklarera klasser.

 

Men nu, när vi programmerar i C++, har vi tillgång till klasser, och nu ska vi definiera vår första klass:

 

class CCirkel

{

public:

    CCirkel(float r);

    void  SetRadie(float r);

    float GetRadie(void);

    void  VisaYta(void); 

    ~CCirkel();

private:

    void  BeraknaYta(void);

    float m_Radie;

    float m_Yta;

    float m_PI;

};

 

Detta var mycket på en gång, men låt oss dissikera klassen steg för steg. Till att börja med har vi en övergripande struktur som överensstämmer med en struct-beskrivning:

 

struct Cirkel

{

    variabeldeklarationer...

};

 

class CCirkel

{

    deklaration av variabler och funktioner...

};

 

Till att börja med har vi bytt ut nyckelordet 'struct' mot 'class'. Därefter kommer skillnaden att alla klasser av hävd får ett extra inledande C i namnet, för att man ska se att det är en klass.

 

I strukturdefinitionen kan vi bara lägga in variabler, medan vi har både variabler och funktioner och/eller funktionsprototyper i klassdefinitionen.

 

Om vi betraktar den del där deklarationerna finns ser vi två nya nyckelord, 'public:' och 'private:'.

 

class CCirkel

{

public:

    deklaration av variabler och funktioner...

private:

    deklaration av variabler och funktioner...

};

 

Följande gäller för dessa nyckelord:

 

·     Alla funktioner och variabler som deklarerats efter 'public:' kan användas direkt av anropande program, de är externt kända.

·     Alla funktioner och variabler som deklarerats efter 'private:' är gömda för anropande program, de blir inkapslade i det objekt som sedemera skapas med denna klass som mall.

·     Man kan använda 'public:' och 'private:' omväxlande, de fungerar som switchar som tillgängliggör respektive gömmer funktioner och variabler.

·     När inget annat angivits gäller 'private:', men skriv ändå ut ordet ‘private:' för tydlighets skull.

·     Det finns ett tredje nyckelord som heter ‘protected:’, vilket får sin mening först vid klassarv (se avsnittet ‘Klassarv’). En medlem som är protected i en förälderklass blir tillgänglig i en deriverad klass (en klass som ärvt av förälderklassen), men är gömda för de objekt man skapar m.h.a. den deriverade klassen.

 

Deklarationerna gäller vanliga variabeldeklarationer, funktionsdeklarationer eller funktionsprototyper (funktionerna måste i så fall deklareras senare) samt två speciella funktioner som har samma namn som klassen:

 

public:

    CCirkel();

    ...

    ~CCirkel();

 

Till att börja med konstaterar vi att de ligger under 'public:'. De måste kunna anropas utifrån. De har samma namn som klassen, men den sista har ett '~' framför namnet. (Detta tecken kallas 'tilde' och finns till höger om 'Å'. Man måste hålla nere 'Alt Gr'. Tecknet syns inte förrän man skriver nästa tecken.)

 

Dessa två funktioner kallas 'constructor' respektive 'destructor' . De måste finnas med, men behöver inte innehålla någon kod. Finns det någon kod, så är det den kod som man vill ska utföras när man skapar ett objekt respektive när man tar bort det.

 

En constructor anropas när objektet skapas.

 

Destruktorn anropas när objektet tas bort. Detta sker automatiskt när programmet avslutas, eller om objektet självt anropar destruktorn. Det sistnämnda är ett vanligt fall under windows: ett objekt kan vara t.ex. ett fönster. När användaren stänger fönstret skickar windows ett meddelande till objektet som säger att det ska förstöra sig självt genom att anropa destruktorn. Därefter tar windows bort fönster-objektet.

 

Constructor och destructor måste bägge vara av typen void, men man ska inte ange detta. Gör man det får man ett kompileringsfel.

 

Resten är de funktioner och variabler som vi vill att objekt som skapas med denna klass som mall ska innehålla. Dessa funktioner och variabler kallas medlemmar: medlemsfunktioner och medlemsvariabler. Man brukar använda 'm_' i början av medlemsvariablerna. Medlemsfunktioner kallas också ibland för 'metoder'.

 

Normalt brukar man definiera alla klasser i headerfiler, en headerfil för varje klass och en tillämpningsfil för medlemsfunktionerna. I detta exempel skulle vi lagt klassdefinitionen av CCirkel i en fil kallad cirkel.h och medlems­funk­tion­er­na i cirkel.cpp. Om man har större projekt har man dessutom en separat till­ämp­ningsfil för huvudprogrammet. Nu nöjer vi oss dock med att lägga allt i samma fil.

 

Vi börjar med vår constructor:

 

CCirkel::CCirkel(float r)

{

    m_Radie = r;

    m_PI = 3.14159F;

    BeraknaYta();

}

 

Som synes kan man ange en radie när objektet skapas, så sätts medlems­variab­eln m_Radie till angivet värde redan i constructorn.

 

Lägg också märke till att det är tillåtet att initiera variabler i metoderna: m_PI tilldelas här värdet 3.14159. Detta utföres alltså först när respektive objekt skapas. Kom ihåg att det inte gick att initiera variabler i en struct-definition. Struct-definitionen gav oss ju bara en mall, inga egentliga variabler, så dessa kunde inte initieras. Metoderna är dock kod, som kopieras in i minnet vid första objekt man definierar av klassens typ, och kan alltså innehålla initieringar.

 

Destructorn innehåller däremot ingen kod. Vi skriver dock den tomma funktionen för att se syntaxen men det är inte nödvändigt att den finns. I destructorn brukar man lägga kod för att frigöra minne om man har en pekare i objektet.

CCirkel::~CCirkel()

{

}

 

Lägg märke till att vi talar om vilken klass funktionen tillhör genom att ange klassens namn före funktionens namn, åtskilda av ett dubbelt kolon (::), s.k. räckviddsoperator (Scope Resolution Operator). Därigenom är det alltså möjligt att ha samma namn i flera olika klasser, man kan alltid skilja dem åt på detta sätt.

 

När man i C deklarerade en lokal variabel med samma namn som en global dito, blev den globala variabeln 'osynlig' inom räckvidden för den miljö (funktion) där den deklarerades. I C++ har vi möjlighet att referera till den globala variabeln i alla fall, genom att ange räckviddsoperatorn före variablenamnet.

 

Då kan vi fylla på med resten av funktionerna. Vi vill gärna kunna ändra radien i efterhand:

 

void CCirkel::SetRadie(float r)

{

    m_Radie = r;

    BeraknaYta();

}

 

Därmed uppstår behovet av funktionen BeraknaYta():

 

void CCirkel::BeraknaYta()

{

    m_Yta = m_PI * m_Radie * m_Radie;

}

 

Vi har en funktion för att visa storleken på ytan:

 

void CCirkel::VisaYta(void)

{

    cout << "Cirkelns yta är: " << m_Yta << ‘\n’;

}

 

Och därmed har vi alla bitarna klara. Nu är det bara att prova detta genom att skapa objekt i main(), vilket vi ska göra strax, i nästa avsnitt, men först tar vi och tittar på vårt tidigare exempel med datum. Det skulle kunna se ut så här:

 

#include <iostream.h>

 

class CDatum

{

public:

    CDatum(int a, int m, int d);

    void VisaDatum();

    ~CDatum();

private:

    int m_Aar;

    int m_Maanad;

    int m_Dag;

};

 

Här finns i stort sett vad vi hade ovan, men vi måste naturligtvis även skriva funktionerna. I klassdeklarationen brukar man bara skriva funktionsprototyper, för tydlighets skull.

 

Vi ser vår utskriftsfunktion, men vi ser också konstruktorn, CDatum och destruktorn, ~CDatum.

 

CDatum anropas ju automatiskt när objektet skapas. Argument till denna funktion, vilken kallas konstruktorn, kan anges vid deklaration av objektet:

 

CDatum foedelse_dag(1951, 3, 25);

 

~CDatum anropas som sagts när objektet förstörs. Detta sker när programmet lämnar det sammanhang där objektet skapades, eller, om objektet skapts m.h.a. 'new', när man gör 'delete' på det. Här har man möjlighet att städa sådant som behöver städas, t.ex. om man allokerat minne kan man återlämna det här.

 

Observera att både konstruktor och destruktor är typlösa. De har ingenstans att returnera något värde, och man får inte ens ange att de är av typen 'void'. En konstruktor returnerar inte ett värde, den skapar ett objekt. En destruktor returnerar inte heller ett värde, den förstör ett objekt.

 

Vi kan fortsätta med att deklarera funktionerna:

 

CDatum::CDatum(int a, int m, int d)

{

    static int ant_dagar[12] = {31, 28, 31, 30, 31, 30,

                                31, 31, 30, 31, 30, 31};

    if(a < 100) a += 1900;

    if(a < 1900) a = 1900;

    if(a > 2100) a = 2100;

 

    if(m < 1) m = 1;

    if(m > 12) m = 12;

 

    if(d < 1) d = 1;

    if(m == 2 && !(a % 4)) // Februari, skottår.

    {

        if(d > 29) d = 29;

    }

    else

    {

        if(d > ant_dagar[m - 1]) d = ant_dagar[m - 1];

    }

 

    m_Aar = a;

    m_Maanad = m;

    m_Dag = d;

}

 


void CDatum::VisaDatum()

{

    char cMaanad[12][10] =

                {"Januari", "Februari", "Mars",

                 "April",   "Maj",      "Juni",

                 "Juli",    "Augusti",  "September",

                 "Oktober", "November", "December"};

 

    cout << m_Dag << ' ' << cMaanad[m_Maanad - 1]

         << ' ' << m_Aar << '\n';

}

 

CDatum::~CDatum()

{

    // Fyll i eventuell städning här.

}

 

Sedan är det bara att använda klassen. Exempel på hur vi använder klassen CDatum kommer i avsnittet ‘Använda ett objekt’.

 


Skapa objekt.

 

Man skapar ett objekt ungefär på samma sätt som man deklarerar en variabel av en viss struct-typ. Om vi har deklarerat klassen CCirkel kan vi deklarera t.ex. objektet Frisbee på följande sätt:

 

CCirkel Frisbee(250.0F);

 

Skillnaden ligger i att vi kan passera en variable till constructorn, i detta fall en float, som ska lagras i m_Radie.

 

Vi kan fortsätta med att skapa en ring:

 

CCirkel Ring(12.5F);

 

Varje gång vi skapar ett objekt används klassen som mall för att lägga upp medlemsfunktionerna, medlemsvariablerna, samt att anropa det nya objektets constructor.

 

Varje objekt blir alltså ett fristående objekt. Ändrar man i det ena objektets medlemsvariabel påverkar det inte andra objekt.

 

Alltså kan vi be t.ex. medlemsfunktionen VisaYta() i respektive objekt att tala om för oss vilken yta respektive cirkel har.

 

Funktionerna däremot delas av alla objekt. För t.ex. tre olika objekt av klassen CCirkel förekommer alla variabler i tre uppsättningar tillsammans med respek­tive objekt, medan funktionerna lagras i endast en uppsättning i minnet. När ett objekt aktiverar en funktion meddelar det vilket objekts medlemmar den ska arbeta på. Vi kommer att fördjupa oss mer i detta ämne i nästa kapitel.


Använda objekt.

 

I tidigare avsnitt skapade vi två objekt, Frisbee och Ring, av klassen CCirkel. Nu är det fritt fram att använda de publika medlemsfunktionerna VisaYta() och SetRadie(). Vi kan t.ex. visa hur stor yta varje objekt har:

 

Frisbee.VisaYta();

Ring.VisaYta();

 

Om vi vill ändra radien i objektet ring kan vi göra det m.h.a. metoden SetRadie(), och p.s.s. som tidigare se vad detta ger för yta:

 

Ring.SetRadie(10.0F);

Ring.VisaYta();

 

Övningsuppgift:

·      Skapa projektet cirkel.

·      Skriv ett program som använder klassen cirkel på samma sätt som diskuterats ovan.

·      Utskriften ska se ut så här:

 

                

 

Vi ska också titta på klassen CDatum. Vi har ännu inte skapat några objekt, så vi börjar med detta. Vi kan skriva en main() som testar att skapa två objekt, ett med korrekt datum och ett med felaktigt datum:

 

void main()

{

    CDatum idag(96, 07, 02); // Korrekt.

    CDatum fel(96, 21, 00); // Felaktigt.

    idag.VisaDatum();

    fel.VisaDatum();

}

 

Och så här blev utskriften:

 

                            

I klassen CDatum finns nu bara enklast möjliga felkorrigering. Man kan naturligtvis göra den mer utförlig, varna användaren etc. I detta exempel demonstreras bara att ett felaktigt datum inte kan ta sig in i klassens medlemsvariabler. Observera att de är gömda under nyckelordet 'private', så att man inte heller kan ändra dem utifrån. Klassen har 100% kontroll över deras integritet.

 

Detta är t.ex. inte möjligt att göra i main():

 

m_Dag = -2;      // 'undeclared identifier'

idag.m_Dag = -2; // 'error C2248: 'm_Dag' : cannot access

                     private member declared in class 'cdatum''

 

Åter till CCirkel. Varför deklarerade vi nu våra medlemsvariabler som privata? Ville vi ändra radien, som vi gjorde i exemplet med cirkeln, blev vi tvugna att anropa en publik medlemsfunktion som ändrade värdet. Skulle man inte kunna deklarera variablerna som publika och ändra t.ex. radien direkt:

 

m_Radie = 10;

 

Det är klart att detta går, men vi ska utnyttja möjligheterna till flexibilitet maxi­malt. Och det är flexibelt med inkapslade variabler som påverkas endast genom medlemsfunktioner. Vi som använder klassen behöver inte veta så mycket om variablerna. Det går att ändra i en klass utan att man behöver ändra i de program som använder klassen.

 

Ponera att vi fått klassen CCirkel från någon annan programmerare. När prog­rammeraren senare kanske upptäcker att han behöver datatypen double i stället för float kan han ändra detta i klassen utan att vi behöver ändra i våra program. Har vi inkluderat hans klass med en #include-sats behöver vi bara kompilera om programmet, så har vi också tillgång till hans förbättringar.

 

Vi ska strax testa att göra detta i ett annat exempel, CRektangel.

 


Som du kanske lagt märke till så deklarerade vi en funktionsprototyp vid namn GetRadie() i vår klass CCirkel. Vi skrev dock aldrig själva funktionen, och detta ska vi prova i nästa övningsuppgift:

 

Övningsuppgift:

·      Öppna projektet cirkel.

·      Lägg till en fullständig beskrivning av funktionen GetRadie().

·      Testa i main att läsa av radien för objektet Ring före och efter det att vi ändrat den.

·      Utskriften bör nu se ut så här:

 

                

 

Den som läst noga om referensvariabler bör nog varnas här för att inte vara alltför kreativ:

 

Använd inte accessfunktioner med referensvariabel som returvärde. Det öppnar medlemsvariablen för direkt påverkan, eftersom funktionsanropen blir en alias för medlemsvariabeln.

 

Om man t.ex. skapar accessfunktionen GetRadie() så här:

 

int &GetRadie()

{

    return m_Radie;

}

 

Då skulle man kunna ändra m_Radie i objektet Ring så här:

 

Ring.GetRadie() = 17;

 

eller:

 

Ring.GetRadie()++;

 

Detta förstör inkapslingen av data, vilket var en av huvudidéerna med OOP.


Överkursuppgift:

·      Projekt klot.

·      Skapa en klass i separat fil klot.h.

·      Klassen ska ta emot radien och specifika vikten (densiteten).

·      Man ska kunna fråga efter diameter, omkrets, volym samt massa.

·      Skapa en main() i klot.cpp, vilken skapar ett standardklot med diametern 1 meter och specifika vikten 1 kg / liter.

·      Man ska bli presenterad en meny där man kan välja mellan att:
1 - Ange ny radie.
2 - Ange ny specifik vikt.
3 - Visa uppgifter.
4 - Avsluta.

·      Om man väljer 'Visa uppgifter' ska man få följande skärmutskrift:
Ett klot med radien <radie> m och specifika
vikten <specifik vikt> kg/l har följande data:

Diameter: <diameter> m.
Omkrets: <omkrets> m.
Massa: <massa> kg.


Nu ska vi skapa klassen CRektang, vilken nämndes ovan. Denna klass kommer vi att använda till att demonstrera såväl kompilerade klasser som klassarv i kommande avsnitt. Det är därför nödvändigt att du utför uppgifterna exakt enligt beskrivningen. Deklarera bara de funktioner och variabler som verkligen står i uppgiften, inte sådant du själv tycker kan vara bra att ha. Det är också viktigt att du beskriver hela klassen i en headerfil, först klassbeskrivningen med medlemmarna och funktionsprototyperna och därefter funktionsdeklarationerna. Sedan ska main() stå i en egen fil rektang.cpp. Använd de filnamn som angivits.

 

Övningsuppgift:

·      Skapa ett projekt rektang.

·      Definiera en klass CRektangel i separat headerfil: rektang.h (även funktionerna).

·      CRektangel ska ha två medlemsvariabler av typen int: m_Bredd och m_Hojd, dessa ska vara privata.

·      Den ska förutom construktor och destructor ha en publik funktion som beräknar och skriver ut ytan (OBS! fortfarande i samma headerfil).

·      Skapa en main() som provar klassen CRektangel m.h.a. två objekt, som vi gjorde i exemplet cirkel. Objekten ska heta Golv (2*5m) samt Dorr (1*2m).

·      Utskriften ska se ut så här:

 

                

 

I nedanstående övningsuppgift kan vi testa möjligheten att ändra datatyp i en klass, utan att vi behöver ändra i huvudprogrammet. Vi kommer att få en varning när vi kompilerar som betyder att kompilatorn vill att vi skall observera att vi försöker lagra ett flyttal i en heltalsvariabel, men det kan vi bortse ifrån. Vill vi ändå bli av med den här varningen kan vi lägga till ett kompilerings-direktiv som ”stänger av” just den här varningen:

 

#pragma warning(disable:4244)

 

 

Övningsuppgift:

·      Ändra i rektang.h så att variablerna är av typen float i stället för int (använd Replace i Edit-menyn).

·      Ändra ingenting i rektang.cpp!

·      När du kompilerat, länkat och testar, ska det se ut precis som förut:

 

                     

 


Men nu har vi tillgång till decimaltal (float)! I nästa övning är det bara att tillämpa:

 

Övningsuppgift:

·      Ändra i rektang.cpp så att objektet Golv har dimensionen 3.25m * 4.87m.

·      Ändra dörren till 0.80m * 2.15m.

·      Ändra inte i rektang.h.

·      Utskriften ska bli så här:

 

                     

 

Fördelen med att kapsla in medlemsvariablerna är alltså bevisade. Vi har redan tidigare sett att det inte föreligger några problem med att ändra värdet i en privat medlemsvariabel genom att använda en publik medlemsfunktion (metod). Man gör naturligtvis på motsvarande sätt om man vill hämta värdet i en privat med­lemsvariabel. Detta har vi redan sett när vi använde klassen CCirkel.


Constructor/destructor.

 

När skapas då objektet, d.v.s. när körs konstruktorn? Och när förstörs det? Låt oss prova m.h.a. en enkel klass som bara rapporterar när objekt skapas respek-
tive förstörs. Vi testar att göra fyra objekt, ett globalt, ett som är lokalt i main(), ett som är lokalt i en annan funktion vilken anropas från main() samt ett som är statiskt i samma funktion:

 

#include <iostream.h>

#include <string.h>

 

class CGodDagADieu

{

public:

    CGodDagADieu(const char *namn);

    ~CGodDagADieu();

private:

    char m_Namn[32];

};

 

CGodDagADieu::CGodDagADieu(const char *namn)

{

    strncpy(m_Namn, namn, 31);

    cout << m_Namn << " säger god dag!\n";

}

 

CGodDagADieu::~CGodDagADieu()

{

    cout << m_Namn << " säger adjö!\n";

}

 

void funk()

{

    CGodDagADieu LokaltIFunk("Lokalt objekt i funk");

    static CGodDagADieu LokaltStaticIFunk(

                              "Lokalt statiskt objekt i funk");

   

    cout << "Vi är i funk!\n";

}

 

CGodDagADieu Globalt("Globalt objekt");

   

void main()

{

    CGodDagADieu LokaltIMain("Lokalt objekt i main");

 

    cout << "Vi är i main före anrop till funk!\n";

    funk();

    cout << "Vi är i main efter anrop till funk!\n";

}

 


Så här blir utskriften:

 

                

 

 

 

Observera följande:

 

·     Det globala objektet skapas innan main() körs.

·     Objektet i main() skapas före objekten i funk().

·     Objekten i funk() skapas när funk() anropats från main().

·     Det lokala objektet i funk() förstörs så fort programmet lämnar dess sammanhang (scope), innan kontrollen återlämnas till main().

·     Det lokala objektet i main() förstörs när main går ur scope.

·     Det statiska objektet i funk() förstörs först efter att main() gått ur scope.

·     Det globala objektet förstörs sist.

 

Det är vanligt att man överlagrar konstruktorn. Om vi tittar på exemplet med CCirkel, så skulle vi kunna förse ett objekt med en standardradie när objektet skapas. Man kanske vill bestämma radien vid ett senare tillfälle, men vi ska absolut inte tillåta att objekt skapas med felaktiga data. En cirkel kan t.ex. inte ha en negativ radie.

 

Frågar man en matematiker finns det en självklar standardradie, nämligen den i enhetscirkeln. Denna har en radie av 1. Alltså lägger vi till en andra konstruktor i CCirkel, en som inte tar några argument, och deklarerar sedan dess kod:

 

CCirkel::CCirkel()

{

    m_Radie = 1;

    m_PI = 3.14159;

    BeraknaYta();

}

 

Som synes ser den i övrig exakt lika dan ut som den konstruktor vi redan deklarerat. När man deklarerar en överlagrad konstruktor som inte tar några argument brukar den kallas ‘default constructor’. Observera att syntaxen när man kallar på default contructor är utan paranteser:

 

Cdatum datum;

 

Övningsuppgift:

·      Skapa en överlagrad konstruktor till CCirkel enligt ovanstående modell.

·      Testa den från main(). Om man frågar efter ytan borde den bli 3.14159.

 

Övningsuppgift:

·      Skapa ett nytt projekt datum för klassen CDatum.

·      Försök att skapa klassen på egen hand, utan att titta i häftet.

·      Du behöver inte hantera skottår om du inte vill, alla Februari får vara 28 dagar.

·      Lägg till en överlagrad konstruktor som inte tar några argument, och låt den initiera datum till 1984-01-01.

·      Testa att skapa tre objekt i main, ett med korrekt datum, ett med ett felaktigt datum samt ett utan datum. Skriv ut alla tre datum på skärmen.

·      Så här kan det se ut:

 

                


Konstanta objekt.

 

Precis som man kan deklarera konstanta variabler m.h.a. nyckelordet 'const', kan man även deklarera konstanta objekt på samma sätt.

 

Kompilatorn har däremot inte samma möjligheter att avgöra vilka medlemsfunktioner som kan tänkas påverka variabler i objektet, och förbjuder för säkerhets skull all användning av medlemsfunktioner på ett konstant objekt. Detta är naturligtvis inte helt bra, eftersom vi gärna skulle vilja kunna skapa funktioner som vi själva ansvarar för att de inte påverkar visst data i objektet.

 

Genom att lägga till nyckelordet 'const' efter parameterlistan i en medlemsfunktion 'lovar' man att funktionen inte förändrar data, vilket kompilatorn kontrollerar. Om man sedan skapar ett konstant objekt av klassen, tillåter kompilatorn användning av alla funktioner som deklarerats på detta sätt.


Peka på objekt.

 

Det är ofta praktiskt att referera till ett objekt m.h.a. en pekare i stället för ett namn. Därigenom kan man t.ex. dynamiskt reservera minne till ett i förväg okänt antal objekt. Tänk t.ex. på ritverktyget i Word. Varje gång man ritar en cirkel, eller ett annat grafiskt objekt, skapas ett nytt objekt med motsvarande klass som förebild. Eftersom det inte har något namn, gör man alla metodanrop via en pekare till objektet.

 

Vi testar detta genom att ändra i exemplet cirkel från tidigare.

 

Lägg till följande rader direkt efter deklarationen av objekten:

 

CCirkel * p1 = &Frisbee;

CCirkel * p2 = &Ring;

 

Ändra alla metodanrop så att pekaren används i stället för objektets namn, t.ex:

 

p1->VisaYta();

p2->VisaYta();

 

Nu kan main() anropa alla metoder utan att använda objektens namn.

 

Övningsuppgift:

·      Gör ovannämnda ändringar i cirkel.c.

·      Kompilera, länka och testa.

·      Allt ska se ut precis som förut:

 

 

 

                     

 

 

 

 

 

 

? Pointer to member of class, pointer to member operator.


New och Delete.

 

I föregående avsnitt skapade vi pekare som pekade på redan skapade objekt, vilka därigenom redan hade fått sitt namn. Nu ska vi skapa objekt utan namn. Detta gör man genom att använda 'new', på samma sätt som vi lärt oss att reservera minne för namnlösa variabler. Vi fortsätter att ändra i cirkel.cpp. Ta bort objektdefinitionerna, d.v.s. nedanstående rader:

 

CCirkel Frisbee(250.0F);

CCirkel Ring(12.5F);

 

Ändra raderna...

 

CCirkel * p1 = &Frisbee;

CCirkel * p2 = &Ring;

 

...till:

 

CCirkel * p1 = new CCirkel(250.0F);

CCirkel * p2 = new CCirkel(12.5F);

 

Nu har vi två icke namngivna objekt, vilka används enbart genom pekare. Vis­serligen har vi namngivit pekarna, men dessa kan man faktiskt lägga i en lista för sig, vilken kan bestå av dynamiskt reserverat utrymme.

 

För stunden tränger vi inte in djupare än så här i ämnet. Vi har ju redan provat på dynamisk minnesallokering i det första häftet, när vi konstruerade en länkad lista. Samma princip kan gälla för andra program som använder ett variabelt antal objekt, t.ex. ett objektorienterat ritprogram.

 

Däremot måste man tänka på att själv frigöra det minne som använts. Detta är helt och hållet programmerarens uppgift, kompilatorn kan inte känna igen sådana fel. Det är heller inte tillåtet att frigöra minnet mer än en gång. Programmet skulle i så fall uppföra sig konstigt, och antagligen krascha. Däremot är det tillåtet att använda ‘delete’ med en pekare som innehåller adressen 0. Man kan därför för säkerhets skull initiera alla pekare som är avsedda att få sin adress från ‘new’ med NULL, om man inte ger dem en adress med det samma.

 

Nu tar vi bort våra objekt i detta exempel. Lägg därför till detta i cirkel.cpp:

 

delete p1;

delete p2;

 

Övningsuppgift:

·      Testa ovanstående ändringar i cirkel.cpp.


Kompilerade klasser.

 

Vi såg i föregående avsnitt hur en klass kunde ändras utan att man behövde göra motsvarande ändringar i det program som använder klassen. En programvaru­leverantör som levererar färdiga klasser, t.ex. Microsoft som levererar MFC, har därigenom frihet att rätta, ändra, förbättra och göra tillägg i de klasser vi använ­der i våra program, utan att vi behöver tänka på det.

 

Nu ska vi testa att använda en färdigkompilerad klass. Vi behöver alltså en klass som vi kan kompilera, kopiera själva klassdefinitionen till en headerfil (utan att ta med metoderna). Sedan ska vi sudda den ursprungliga källkoden, så att vi kan se att vi klarar oss utan den.

 

Vi tar och använder oss av CRektang, vilken vi redan skapat i ett övningsexem­pel ovan.

 

Först skapar vi ett nytt projekt som vi kallar rektang2.

 

Vi kopierar ...\rektang\rektang.h till ...\rektang2\crektang.cpp. Observera att vi använder filnamnstillägget .cpp, detta för att vi ska kunna kompilera den. Nu var det dock så att vi deklarerade medlemsvariablerna som privata. Detta håller inte längre, eftersom vi vill kunna lägga till egna metoder som påverkar dessa variabler. Byt därför ut nyckelordet 'private:' mot ‘protected:’ i klassdeklara­tionen.

 

Kompilera crektang.cpp så att vi får en fil crektang.obj. Länka inte!

 

Kopiera klassbeskrivningen ur crektang.cpp och skapa en ny fil crektang.h där du klistrar in det kopierade avsnittet. Crektang.h ska nu innehålla:

 

class CRektangel

{

public:

    CRektangel(float b, float h);

    void VisaYta(void);

    ~CRektangel();

protected:

    float m_Bredd;

    float m_Hojd;

};

 

Nu kan vi ta bort crektang.cpp. Den behövs inte längre. Det är den som motsvarar det vi inte får när vi köper MFC.

 

Huvudprogrammet, som vi skapar själva, kan vi kopiera från ...\rektang\rek­tang.cpp till ...rektang2\rektang2.cpp. Ändra i #include-satsen så att den tar med "crektang.h" i stället för "rektang.h". Passa också på att rätta ordet "Dorr" till "Dörr" i utskriften.

 

Nu när vi har alla filer på plats kan vi sätta in filerna rektang2.cpp och crektang.obj i projektet.

 

Spänningen är olidlig nu när vi kompilerar, länkar och testar. Men det fungerar. Det går alltså att klara av, trots att vi bara har metoderna (medlemsfunktioner­na) i objektsform, precis som när vi har köpt färdiga klasser, t.ex. MFC.

 

På samma sätt som vi använder färdigkompilerade funktioner, t.ex. printf(), där vi inte har källkoden, bara själva prototypen, har vi nu kunnat använda en färdigkompilerad klass. Klassen vi använde fanns i objektsform, och vi talade om för projektet att det skulle ta med den filen.

 

Hade vi i stället använt oss av någon klass ur MFC, vilket vi kommer att göra i kursen’ Windowsprogrammering med MFC’, tar vi inte med klassens objektfil i projektets fillista. I stället väljer vi att skapa ett projekt av typen ‘MFC Applica
tionWizard (exe)’. Developer Studio 'vet' vilka objektsfiler som behövs.

 

Övningsuppgift:

·      Skapa rektang2 exakt enligt ovanstående beskrivning.

·      Kompilera, länka och testa så att det fungerar som det ska, vi kommer nu att bygga vidare på detta projekt.

 


Klassarv.

 

Men om vi nu vill göra egna tillägg i de klasser vi köpt? (MFC står för Micro­soft Foundation Classes och innehåller grundläggande klasser för windows­pro­gram­mering. Vi får t.ex. ett tomt programfönster, men behöver lägga till diverse funktioner, menyer etc.)

 

Låt oss ta ett enkelt exempel: CRektangel i föregående avsnitt. När vi skapar ett objekt av klassen CRektangel  anger vi bredd och höjd. Därefter kan vi inte ändra varken bredd eller höjd, de är fastlåsta vid de värden vi använde när vi skapade objektet.

 

När vi skapade CCirkel lade vi in en metod för att ändra radien, SetRadie(). Samma sak kan man naturligtvis göra med CRektangel.

 

Men om vi inte har källkoden då?

 

Programvaruleverantörer skyddar alltid sina produkter genom att inte leverera källkoden. De skyddar dessutom oss från att göra felaktiga förändringar i de klasser vi köpt, eller förändringar som i framtiden inte passar ihop med nya ver­sioner av t.ex. MFC.

 

För att lösa detta har man infört en möjlighet att deklarera en ny klass och ange att den är av samma typ som en befintlig (och kanske kompilerad) klass, med vissa tillägg, som man själv skriver. Därigenom kommer den nya klassen att ärva alla medlemsvariabler och metoder (medlemsfunktioner) som den angivna "förälderklassen" har. Detta kallas för klassarv (Class Inheritage). Den nya klassen kallas härledd (derived) klass.

 

Det enda man behöver är föräldrarklassen i kompilerad version, samt all doku­men­tation som beskriver de medlemsvariabler och metoder vi har tillgång till.Ett sådant släktträd utgör en s.k. klasshierarki (Class Hierarchy).

 

Man deklarerar en ny klass som arvinge till en existerande klass (man härleder en ny klass från en existerande). Skriv in följande i början av rektang2.cpp, direkt efter de två #include-satserna:

 

class CNyRektangel : public CRektangel

{

public:

    CNyRektangel(float b, float h);

    void SetBredd(float b);

    void SetHojd(float h);

    ~CNyRektangel();

};

Lägg märke till hur det hela gjordes: Vi härledde den nya klassen CNyRektang­el ur klassen CRektangel (vilken vi inte har källkoden till), genom att lägga till ": public CRektangel" på första raden, efter det nya klassnamnet.

 

Vi skrev in prototyper för ny konstruktor, destruktor samt de extra metoder vi vill lägga till.

 

Därefter är det bara att fylla på med de nya metoderna. Vi börjar med konstruktor och destruktor:

 

CNyRektangel::CNyRektangel(float b, float h):CRektangel(b, h)

{

}       

 

CNyRektangel::~CNyRektangel()

{

}

 

Observera att de är tomma. Detta beror av att vi inte tänker lägga till någon ny kod, som utförs när objekten skapas respektive förstörs, än.

 

Konstruktorn innehåller en extra del i funktionshuvudet: ett anrop till den av föräldrarklassen ärvda konstruktorn. Detta är nödvändigt för att vidarepassa de värden som anges för att initiera rektangelns bredd och höjd. Allmän syntax:

 

<class name>::<constructor name>([<argument declaration>]...)

           : <parent class name>([<argument>]...)

 

Vi fortsätter med de nya metoderna:

 

void CNyRektangel::SetBredd(float b)

{

         m_Bredd = b;

}

 

void CNyRektangel::SetHojd(float h)

{

         m_Hojd = h;

}

 

Eftersom vi bytt ut den klass vi använder måste vi byta namn på klassen när vi skapar objekt i main():

 

CRektangel Golv(3.25, 4.87);

CRektangel Dorr(0.80, 2.15);

 

Ändras till:

 

CNyRektangel Golv(3.25, 4.87);

CNyRektangel Dorr(0.80, 2.15);

 

Nu har vi ett program som använder den nya klassen, men utan att använda de nya metoderna. Vi lägger till detta i slutet av main() så att vi kan testa SetBredd() och SetHojd():

 

cout << "\nNu sätter vi nya dimensioner för Golv.\n";

Golv.SetBredd(3.15);

Golv.SetHojd(4.75);

cout << "Golv: ";

Golv.VisaYta();

 

Övningsuppgift:

·      Gör ovanstående tillägg och ändringar i rektang2.

·      Kompilera, länka och testa.

·      Det ska se ut så här:

 

                

 

Observera att datainkapslingen fortfarande håller. Vi kan fortfarande inte komma åt medlemsvariablerna direkt på objektet. Vi får t.ex. inte skriva:

 

Golv.m_Bredd = 3.2; //error C2248: m_Bredd: cannot access...

 

Detta tack vare nyckelordet ‘protected:’.


Klasshierarki.

 

Man kan rita upp förhållandet mellan CRektangel och CNyRektangel på följande sätt:

 

 


                           CRektangel

 

 

 


                          CNyRektangel

 

 

Detta kallas för att rita upp dess hierarki. (Inte harakiri!) Föräldern behöver inte avlida för att barnet ska få ärva.

 

I övrigt liknar faktiskt en klasshierarki ett släktträd:

 

 

 


                         Klass A

 

 

 

                         Klass B

 

 

 

               Klass C            Klass F

 

 

 

               Klass D            Klass G

 

 

 

                         Klass E           Klass H          

 

 

Man kan se vilka klasser som kan anropa vilka medlemsfunktioner genom att studera klasshierarkin. En klass kan anropa en metod i en klass ovanför, men inte i en klass nedanför.

 

Klass H kan alltså anropa metoder i alla klasser utom i klasserna C, D och E. Klass C kan anropa metoder i klass A och B, men inte i klass D och inga klasser i den andra "grenen".

 


Utbyte av medlemsfunktion.

 

En metod är inte orörbar bara för att den ligger färdigkompilerad i en objektsfil. Det är en sak att vi inte kan ändra i den, men vi har möjlighet att byta ut den.

 

Vi skulle t.ex. kunna göra en glassigare utskrift i CNyRektangel genom att byta ut VisaYta(). Gör detta genom att lägga till följande prototyp efter SetHojd():

 

void VisaYta(void);

 

Beskriv sedan själva funktionen och kundanpassa utskriften enligt nedan:

 

void CNyRektangel::VisaYta(void)

{

    float m_Yta = m_Bredd * m_Hojd;

    cout << "\n******************************\n";

    cout << "Rektangelns yta är nu: " << m_Yta;

    cout << "\n******************************\n";

}

 

Om vi sedan kompilerar, länkar och testar får vi följande utskrift:

 

 

 

                

 

 

 

 

Övningsuppgift:

·      Gör ovanstående tillägg.

·      Testa, utskriften ska bli som ovan.

·      Du ska inte ändra något i main().

 


En variant på ovanstående är att använda räckviddsoperatorn till att anropa den del som skriver ut själva texten, så behöver vi bara göra själva tilläggen, d.v.s. stjärnraderna. Det är inte helt ovanligt att man gör så. Vi kommer att stöta på det när vi använder MFC.

 

Så här skulle funktionen se ut i så fall:

 

void CNyRektangel::VisaYta(void)

{

    cout << "\n******************************\n";

    CRektangel::VisaYta();

    cout << "\n******************************\n";

}

 

Observera att vi fortfarande inte kan komma åt privata medlemmar. Om man byter ut en accessfunktion, t.ex. SetRadie i klassen CCirkel, så ger det ändå inte oss möjlighet att påverka variabeln direkt. Endast orginalfunktionen, som deklarerats i originalklassen, kan påverka variabeln. Detta tack vare nyckelordet ‘private:’. Dataintegriteten är således fortfarande 100%-ig.

 

Vill den som skapar klassen lämna dörren öppen för denna möjlighet, byter han ut nyckelordet ‘private:’ mot ‘protected:’, vilket tillåter den ärvande klassen att använda medlemsvariabeln utan att man kan påverka den från objektet. Dess
utom dokumenterar han mycket noga denna möjlighet som klassen nu har. MFC har många sådana exempel.